package com.gitee.feizns.web.mvc.validate;

import com.gitee.feizns.ArrayUtils;
import com.gitee.feizns.convert.ConvertUtils;
import com.gitee.feizns.reflect.ConstructorUtils;
import com.gitee.feizns.reflect.FieldUtils;
import com.gitee.feizns.reflect.MethodUtils;
import com.gitee.feizns.web.boot.validate.ValidateProperties;
import com.gitee.feizns.web.dao.ar.AbstractModel;
import com.gitee.feizns.web.domain.result.Err;
import com.gitee.feizns.web.ex.BadRequestException;
import com.gitee.feizns.web.ex.ServiceException;
import com.gitee.feizns.web.utils.AppUtils;
import org.hibernate.validator.internal.engine.constraintvalidation.ConstraintValidatorContextImpl;
import org.hibernate.validator.internal.engine.path.PathImpl;
import org.springframework.util.StringUtils;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType;
import java.lang.reflect.Field;
import java.util.Map;
import java.util.function.Function;

/**
 * @author feizns
 * @since 2019/7/20
 */
public abstract class AbstractConstraintValidator<A extends Annotation, P> implements ConstraintValidator<A, P> {

    protected A annotation;

    protected ValidateProperties properties;

    @Override
    public void initialize(A annotation) {
        this.annotation = annotation;
        this.properties = AppUtils.getApp().getBean(ValidateProperties.class);
    }

    /**
     * 验证过程：返回false表示验证失败
     * @param model
     * @param field
     * @param value
     * @return
     */
    public abstract boolean validate(AbstractModel model, String field, Object value, String where);

    /**
     * 消息模板
     * @param type
     * @param value
     * @return
     */
    public abstract String getMessage(Class<? extends AbstractModel> type, String field, Object value);

    @Override
    public boolean isValid(Object value, ConstraintValidatorContext context) {
        if ( value == null ) {
            Boolean required = getProp("required", boolean.class);
            if ( required )
                throwBadQue(context, path -> "Parameter " + path + " cannot be empty.");
            return true;
        }

        String code = getProp("code", String.class);
        Class<? extends AbstractModel> type = getProp("value", Class.class);
        AbstractModel model = getModel(type);
        String field = getField(model, context);

        Object[] values = ArrayUtils.toArray(value);

        if ( values.length > properties.getMaximumVerificationArrayLength() )
            throwBadQue(context, path -> String.format("Parameter %s length cannot be greater than %s.", path, properties.getMaximumVerificationArrayLength()));

        int i = validateAny(values, model, field, getProp("where", String.class));

        if ( i != -1 ) {
            field = com.gitee.feizns.StringUtils.underlineToHump(field);
            String message = getMessage(type, field, values[i]);
            context.disableDefaultConstraintViolation();
            context.buildConstraintViolationWithTemplate(message).addConstraintViolation();

            if ( properties.isCustomizeErrorCode() ) {
                if ( StringUtils.hasText(code) )
                    throw new ServiceException(properties.getValidateErrorMapping().get(code));
                else {
                    Err err = getErrorByType(type);
                    if ( err != null )
                        throw new ServiceException(getErrorByType(type));
                }
            }
            throw new BadRequestException(message);
        }

        return true;
    }

    /**
     * 返回 -1 表示成功.
     * @param values
     * @param model
     * @param field
     * @return
     */
    private int validateAny(Object[] values, AbstractModel model, String field, String where) {
        for (int i = 0; i < values.length; i++) {
            if ( validate(model, field, values[i], where) == false )
                return i;
        }
        return -1;
    }


    private void throwBadQue(ConstraintValidatorContext context, Function<String, String> pathSupplier) {
        String path = getPathName(FieldUtils.getData(context, "basePath"));
        String msg = pathSupplier.apply(path);
        throw new BadRequestException(msg);
    }

    private Err getErrorByType(Class<? extends AbstractModel> type) {
        Map<Class<? extends AbstractConstraintValidator>, Map<Class<? extends AbstractModel>, Err>> mapping = properties.getValidatorEntityErrorMapping();
        if ( mapping != null ) {
            Map<Class<? extends AbstractModel>, Err> errMap = mapping.get(this.getClass());
            return errMap != null ? errMap.get(type) : null;
        }
        return null;
    }

    private AbstractModel getModel(Class<? extends AbstractModel> type) {
        return ConstructorUtils.newInstance(type);
    }

    /**
     *
     * @param model
     * @param context
     * @return
     */
    private String getField(AbstractModel model, ConstraintValidatorContext context) {
        String field = getProp( "field", String.class);
        if ( field.trim().isEmpty() ) {
            if ( context instanceof ConstraintValidatorContextImpl
                    && ElementType.PARAMETER.equals(
                            FieldUtils.getData(((ConstraintValidatorContextImpl) context).getConstraintDescriptor(), "elementType"))) {
                field = getPathName(FieldUtils.getData(context, "basePath"));
            } else {
                Field pkField = model.pkField();
                if ( pkField == null )
                    throw new FieldNotFoundException("Cannot find the specified field ID.");
                field = pkField.getName();
            }
        }

        return com.gitee.feizns.StringUtils.toUnderline(field);
    }

    private <T> T getProp(String name, Class<T> targetType) {
        return ConvertUtils.to(MethodUtils.invoke(annotation, name), targetType);
    }

    private String getPathName(PathImpl path) {
        return path.getLeafNode().getName();
    }


    /**
     * 找不到字段
     */
    public static class FieldNotFoundException extends RuntimeException {

        public FieldNotFoundException(String message) {
            super(message);
        }

    }

}
